# # app/services/progress_service.py
# from datetime import datetime
# from typing import Optional, Dict
# import json
# from pathlib import Path

# from app.config.base_setting import backend_logger

# # Internal in-memory state
# _progress_state: Dict[str, dict] = {}
# _filename_to_task: Dict[str, str] = {}


# REMOTE_PROGRESS_DIR = Path("/root/PaddleOCR")

# # ──────────────────────────────────────────────────────────────────────────────
# # Task lifecycle operations
# # ──────────────────────────────────────────────────────────────────────────────

# def start_task(task_id: str, total_steps: int = 1, label: Optional[str] = None):
#     """Initialize a new task with total steps and optional label."""
#     backend_logger.debug(f"🆕 [progress_service] Starting task {task_id} with {total_steps} steps, label: {label}")
#     _progress_state[task_id] = {
#         "label": label or "Processing",
#         "total_steps": total_steps,
#         "current_step": 0,
#         "start_time": datetime.utcnow().isoformat(),
#         "status": "in_progress",
#         "error": None
#     }

# def update_task(task_id: str, current_step: int, label: Optional[str] = None):
#     """Update the current step of an existing task."""
#     task = _progress_state.get(task_id)
#     if task:
#         backend_logger.debug(f"🔁 [progress_service] Updating task {task_id}: step {current_step}, label: {label}")
#         task["current_step"] = current_step
#         if label:
#             task["label"] = label

# def complete_task(task_id: str):
#     """Mark a task as completed."""
#     task = _progress_state.get(task_id)
#     if task:
#         task["status"] = "completed"

# def fail_task(task_id: str, error_message: str):
#     """Mark a task as failed and store the error."""
#     task = _progress_state.get(task_id)
#     if task:
#         task["status"] = "failed"
#         task["error"] = error_message

# # ──────────────────────────────────────────────────────────────────────────────
# # Query by task_id
# # ──────────────────────────────────────────────────────────────────────────────

# def get_progress(task_id: str) -> dict:
#     """
#     Return progress info for a task ID.
#     Includes percent, label, status, and error if any.
#     """
#     task = _progress_state.get(task_id)
#     if not task:
#         return {"status": "unknown", "percent": 0, "label": "Unknown task", "error": None}

#     return _build_progress(task_id, task)

# # ──────────────────────────────────────────────────────────────────────────────
# # Query by filename
# # ──────────────────────────────────────────────────────────────────────────────

# def register_filename(filename: str, task_id: str):
#     """Map a filename to a task ID for later lookup."""
#     _filename_to_task[filename] = task_id

# def get_progress_for_file(filename: str) -> dict:
#     """
#     Get progress info based on a filename (via linked task_id).
#     Logs warnings if missing.
#     """
#     task_id = _filename_to_task.get(filename)
#     if not task_id:
#         backend_logger.warning(f"[progress_service] No task_id for filename: {filename}")
#         return {"percent": 0, "status": "not_started", "label": "Not started", "error": None}

#     task = _progress_state.get(task_id)
#     if not task:
#         backend_logger.warning(f"[progress_service] No progress state for task_id: {task_id} (from file: {filename})")
#         return {"percent": 0, "status": "not_started", "label": "Waiting to start", "error": None}

#     result = _build_progress(task_id, task)
#     backend_logger.info(f"[progress_service] File '{filename}' (task_id: {task_id}) progress: {result}")
#     return result

# # ──────────────────────────────────────────────────────────────────────────────
# # Helper
# # ──────────────────────────────────────────────────────────────────────────────

# def read_remote_progress(task_id: str) -> dict:
#     progress_path = REMOTE_PROGRESS_DIR / f"progress_{task_id}.json"
#     if not progress_path.exists():
#         return {"error": "Progress file not found", "exists": False}
    
#     try:
#         with open(progress_path, "r", encoding="utf-8") as f:
#             data = json.load(f)
#         data["exists"] = True
#         return data
#     except Exception as e:
#         return {"error": str(e), "exists": False}
    
# def _build_progress(task_id: str, task: dict) -> dict:
#     remote_data = read_remote_progress(task_id)

#     # If there is data remotely, use it first
#     if remote_data.get("exists"):
#         percent = remote_data.get("percent", 0)
#         status = remote_data.get("status", "in_progress")
#         label = remote_data.get("message", "")
#         files = remote_data.get("files", [])
#         error = None
#         for f in files:
#             if f.get("status") == "failed":
#                 error = f"{f['filename']}: {f['message']}"
#                 break
#     else:
#         # fall back to backend progress state
#         total = task.get("total_steps", 1)
#         current = task.get("current_step", 0)
#         percent = min(100, int((current / total) * 100)) if total else 0
#         status = task.get("status", "unknown")
#         label = task.get("label", "")
#         error = task.get("error")

#     return {
#         "task_id": task_id,
#         "percent": percent,
#         "status": status,
#         "label": label,
#         "error": error,
#         "start_time": task.get("start_time"),
#         "files": remote_data.get("files", [])  # Provide complete file level information with task_id to front-end
#     }





# app/services/progress_service.py

from datetime import datetime
from typing import Optional, Dict
from app.config.base_setting import backend_logger
from app.utils.remote_ssh import ssh_connect, read_remote_progress_file
from pathlib import Path
from time import time

# ──────────────────────────────────────────────────────────────────────────────
# Constants
# ──────────────────────────────────────────────────────────────────────────────

REMOTE_PROGRESS_DIR = Path("/root/PaddleOCR")  # 本地路径暂时保留，后面会改成远程

# ──────────────────────────────────────────────────────────────────────────────
# Internal in-memory state
# ──────────────────────────────────────────────────────────────────────────────

_progress_state: Dict[str, dict] = {}
_filename_to_task: Dict[str, str] = {}

# ──────────────────────────────────────────────────────────────────────────────
# Cache with TTL (Time To Live)
# ──────────────────────────────────────────────────────────────────────────────

CACHE_TTL = 2.0  # seconds
_progress_cache: Dict[str, dict] = {}
_cache_expiry: Dict[str, float] = {}


def get_cached_progress(task_id: str) -> Optional[dict]:
    if task_id in _progress_cache and time() < _cache_expiry.get(task_id, 0):
        backend_logger.debug(f"⚡ [progress_service] Cache hit for task {task_id}")
        return _progress_cache[task_id]
    return None


def set_cached_progress(task_id: str, data: dict):
    _progress_cache[task_id] = data
    _cache_expiry[task_id] = time() + CACHE_TTL


def clear_cached_progress(task_id: str):
    _progress_cache.pop(task_id, None)
    _cache_expiry.pop(task_id, None)

# ──────────────────────────────────────────────────────────────────────────────
# Remote progress reader (to be replaced with SSH-based version)
# ──────────────────────────────────────────────────────────────────────────────

def read_remote_progress(task_id: str) -> dict:
    """
    模拟远程读取：未来将替换为 SSH 调用远程服务器上的 JSON 文件。
    示例实现，返回模拟数据或空值。
    """
    backend_logger.info(f"📡 [progress_service] Fetching remote progress for {task_id} via SSH")
    client = ssh_connect()
    if not client:
        backend_logger.error("❌ [progress_service] SSH connection failed.")
        return {"exists": False, "error": "SSH connection failed"}

    try:
        data = read_remote_progress_file(client, task_id)
        client.close()
        if data:
            data["exists"] = True
            return data
        else:
            return {"exists": False, "error": "Progress file not found or invalid"}
    except Exception as e:
        backend_logger.exception("❌ [progress_service] Error fetching remote progress")
        return {"exists": False, "error": str(e)}
    
# ──────────────────────────────────────────────────────────────────────────────
# Task lifecycle operations
# ──────────────────────────────────────────────────────────────────────────────

def start_task(task_id: str, total_steps: int = 1, label: Optional[str] = None):
    """Initialize a new task with total steps and optional label."""
    backend_logger.debug(f"🆕 [progress_service] Starting task {task_id} with {total_steps} steps, label: {label}")
    _progress_state[task_id] = {
        "label": label or "Processing",
        "total_steps": total_steps,
        "current_step": 0,
        "start_time": datetime.utcnow().isoformat(),
        "status": "in_progress",
        "error": None
    }


def update_task(task_id: str, current_step: int, label: Optional[str] = None):
    """Update the current step of an existing task."""
    task = _progress_state.get(task_id)
    if task:
        backend_logger.debug(f"🔁 [progress_service] Updating task {task_id}: step {current_step}, label: {label}")
        task["current_step"] = current_step
        if label:
            task["label"] = label


def complete_task(task_id: str):
    """Mark a task as completed."""
    task = _progress_state.get(task_id)
    if task:
        task["status"] = "completed"


def fail_task(task_id: str, error_message: str):
    """Mark a task as failed and store the error."""
    task = _progress_state.get(task_id)
    if task:
        task["status"] = "failed"
        task["error"] = error_message

# ──────────────────────────────────────────────────────────────────────────────
# Query by task_id
# ──────────────────────────────────────────────────────────────────────────────

def get_progress(task_id: str) -> dict:
    """
    Return progress info for a task ID.
    Includes percent, label, status, and error if any.

    Uses cache first, falls back to remote file reading if expired.
    """
    cached = get_cached_progress(task_id)
    if cached:
        return cached

    remote_data = read_remote_progress(task_id)

    result = _build_progress(task_id, remote_data)
    set_cached_progress(task_id, result)
    return result

# ──────────────────────────────────────────────────────────────────────────────
# Query by filename
# ──────────────────────────────────────────────────────────────────────────────

def register_filename(filename: str, task_id: str):
    """Map a filename to a task ID for later lookup."""
    _filename_to_task[filename] = task_id


def get_progress_for_file(filename: str) -> dict:
    """
    Get progress info based on a filename (via linked task_id).
    Logs warnings if missing.
    """
    task_id = _filename_to_task.get(filename)
    if not task_id:
        backend_logger.warning(f"[progress_service] No task_id for filename: {filename}")
        return {"percent": 0, "status": "not_started", "label": "Not started", "error": None}

    task = _progress_state.get(task_id)
    if not task:
        backend_logger.warning(f"[progress_service] No progress state for task_id: {task_id} (from file: {filename})")
        return {"percent": 0, "status": "not_started", "label": "Waiting to start", "error": None}

    result = get_progress(task_id)
    backend_logger.info(f"[progress_service] File '{filename}' (task_id: {task_id}) progress: {result}")
    return result

# ──────────────────────────────────────────────────────────────────────────────
# Helper
# ──────────────────────────────────────────────────────────────────────────────

def _build_progress(task_id: str, remote_data: dict) -> dict:
    task = _progress_state.get(task_id, {})

    if remote_data.get("exists"):
        percent = remote_data.get("percent", 0)
        status = remote_data.get("status", "in_progress")
        label = remote_data.get("message", "")
        files = remote_data.get("files", [])
        error = None
        for f in files:
            if f.get("status") == "failed":
                error = f"{f['filename']}: {f['message']}"
                break
    else:
        # fall back to backend progress state
        total = task.get("total_steps", 1)
        current = task.get("current_step", 0)
        percent = min(100, int((current / total) * 100)) if total else 0
        status = task.get("status", "unknown")
        label = task.get("label", "")
        error = task.get("error")

    return {
        "task_id": task_id,
        "percent": percent,
        "status": status,
        "label": label,
        "error": error,
        "start_time": task.get("start_time"),
        "files": remote_data.get("files", [])  # Provide complete file level information with task_id to front-end
    }




# # app/services/progress_service.py

# import json
# import logging
# from pathlib import Path
# from typing import Dict, List, Optional, Union
# from paramiko import SSHClient, AutoAddPolicy, SSHException

# logger = logging.getLogger(__name__)

# # === CONFIGURABLE PATHS ===
# REMOTE_PROGRESS_DIR = "/mnt/data/progress/"
# REMOTE_PROGRESS_FILE = REMOTE_PROGRESS_DIR + "progress.json"
# REMOTE_PROGRESS_TEMPLATE = REMOTE_PROGRESS_DIR + "progress_{task_id}.json"

# # === SSH SETUP ===
# def get_ssh_client() -> SSHClient:
#     """Creates a pooled or new SSH client."""
#     client = SSHClient()
#     client.set_missing_host_key_policy(AutoAddPolicy())
#     try:
#         client.connect("remote_host", username="user", password="password")  # replace with actual
#         return client
#     except SSHException as e:
#         logger.error(f"SSH connection failed: {e}")
#         raise

# def read_remote_file(path: str) -> Optional[str]:
#     """Read a remote file over SSH. Returns file content or None."""
#     try:
#         ssh = get_ssh_client()
#         sftp = ssh.open_sftp()
#         with sftp.open(path, 'r') as file:
#             content = file.read()
#         sftp.close()
#         ssh.close()
#         return content
#     except Exception as e:
#         logger.warning(f"Failed to read remote file {path}: {e}")
#         return None

# # === PARSERS ===
# def parse_progress_json(raw: str, task_id: Optional[str] = None) -> Dict:
#     """Parse a progress.json or progress_{task_id}.json with backward compatibility."""
#     try:
#         data = json.loads(raw)
#     except Exception as e:
#         logger.error(f"JSON parse error: {e}")
#         return {"percent": 0, "status": "error", "message": "Invalid progress file", "files": []}

#     # Backward compatible structure (flat single-task)
#     if "percent" in data and "status" in data and "files" not in data:
#         return {
#             "task_id": task_id or "unknown",
#             "percent": data.get("percent", 0),
#             "status": data.get("status", "processing"),
#             "message": data.get("message", ""),
#             "files": [{
#                 "filename": data.get("filename", "unknown"),
#                 "progress": data.get("percent", 0),
#                 "status": data.get("status", "processing"),
#                 "message": data.get("message", "")
#             }]
#         }

#     # New structured batch file
#     if "files" in data and isinstance(data["files"], list):
#         return {
#             "task_id": data.get("task_id", task_id or "unknown"),
#             "percent": data.get("percent", 0),
#             "status": data.get("status", "processing"),
#             "message": data.get("message", ""),
#             "files": data["files"]
#         }

#     # Unknown structure fallback
#     logger.warning(f"Unknown progress structure: {data}")
#     return {"percent": 0, "status": "unknown", "message": "Unrecognized progress format", "files": []}

# # === PUBLIC API ===

# def get_batch_progress(task_id: str) -> Dict:
#     """
#     Return progress for a given task_id.
#     Falls back to flat progress.json if progress_{task_id}.json is missing.
#     """
#     # Try task-specific file first
#     path = REMOTE_PROGRESS_TEMPLATE.format(task_id=task_id)
#     raw = read_remote_file(path)
#     if raw:
#         return parse_progress_json(raw, task_id)

#     # Fallback to old flat progress.json
#     raw_fallback = read_remote_file(REMOTE_PROGRESS_FILE)
#     if raw_fallback:
#         return parse_progress_json(raw_fallback, task_id)

#     # Both failed
#     logger.error(f"No progress file found for task {task_id}")
#     return {
#         "task_id": task_id,
#         "percent": 0,
#         "status": "not_found",
#         "message": "No progress found",
#         "files": []
#     }

# def get_all_active_progress_files() -> List[str]:
#     """Lists all active `progress_{task_id}.json` files in the remote directory."""
#     try:
#         ssh = get_ssh_client()
#         sftp = ssh.open_sftp()
#         filenames = [f for f in sftp.listdir(REMOTE_PROGRESS_DIR) if f.startswith("progress_") and f.endswith(".json")]
#         sftp.close()
#         ssh.close()
#         return filenames
#     except Exception as e:
#         logger.error(f"Failed to list remote progress files: {e}")
#         return []

# def get_all_batch_progress() -> List[Dict]:
#     """Returns a list of all task-level progress summaries."""
#     progress_summaries = []
#     filenames = get_all_active_progress_files()
#     for filename in filenames:
#         task_id = filename.replace("progress_", "").replace(".json", "")
#         raw = read_remote_file(REMOTE_PROGRESS_DIR + filename)
#         if raw:
#             parsed = parse_progress_json(raw, task_id)
#             progress_summaries.append(parsed)
#     return progress_summaries

